module ActiveTest
  
  class NoBehaviourSpecified < ActiveTestError #:nodoc:
  end
  
  class NoActionSpecified < ActiveTestError #:nodoc:
  end
  
  class CaseAlreadyDefined < ActiveTestError #:nodoc:
    DEFAULT_MESSAGE = "A dynamic test by that name has already been created.
    To make it unique, add a suffix to the dynamic. For example,
    \"test_should_index_items_for_guests\" has the suffix \"for_guests\"."
    def initialize(message = nil)
      super(message || DEFAULT_MESSAGE)
    end
  end

  # ActiveTest::Spec is the workhourse of anything dynamic. It captures the general
  # specification of behaviour and translates it into a test or series of tests.
  #
  # == Example Dynamic Spec
  #
  #   class ActiveTest::Dynamic::Should
  #     def self.included(base); base.extend(ClassMethods); end
  #     module ClassMethods
  #       def should(operation)
  #         ActiveTest::Spec.define(self) do |spec|
  #           spec.operation = operation
  #           spec.action = discover_action_from_operation(operation)
  #           spec.parameters = parameters
  #         end
  #       end
  #       register_dynamic :should
  #     end
  #   end
  #   ActiveTest::Controller.class_eval do
  #     include ActiveTest::Dynamic::Should
  #   end
  #
  class Spec
    
    attr_accessor :behaviour, :action, :suffix, :options, :params, :before, :after

    def initialize(behaviour = nil)
      self.behaviour = behaviour
    end

    def build(context)
      @context = context
      ensure_minimum_criteria
      @context.send(:define_method, method_name, &method_block)
    end

    # Construct the method name for a specification
    def method_name
      build_method_name("test", behaviour, action, suffix)
    end

    def method_block
      # we need to put everything in local variables?
      behaviour, action, options = self.behaviour, self.action, self.options
      params, before, after = options[:params], options[:before], options[:after]
      Proc.new do
        send(before) if before.is_a? Symbol
        instance_eval(&before) if before.is_a? Proc
        send(operation, action, parameters)
        send(after) if after.is_a? Symbol
        instance_eval(&after) if after.is_a? Proc
      end
    end
    
    protected
    def build_method_name(*args) #:nodoc:
      args.map { |i| (i.nil? || i.blank?) ? nil : i.to_s }.compact.join('_')
    end
    
    def ensure_minimum_criteria
      raise NoBehaviourSpecified unless self.behaviour
      raise NoActionSpecified unless self.action
      raise CaseAlreadyDefined if @context && @context.method_defined?(method_name.to_sym)
    end
    
  end
end